﻿using System;

namespace Infrastructure.CrossCutting.Caching
{
    /// <summary>
    /// All cache items set by AggregateCache(with aggregateKey) will be in an aggregate, 
    /// thus we can clear all items in that aggregate only.
    /// </summary>
    public class AggregateCache: ICache
    {
        private const string KeyHeader = "Aggregatekey";
        private readonly ICache _cache;
        private readonly string _aggregateKey;

        private static long CreateNewToken()
        {
            // Primitive types are more efficient than GUID when processing by memcached.
            // We don't need the precision here, but Stopwatch is more efficient than DateTime.UtcNow.
            // There can be conflicts of the token generated on different web servers,
            //   but it happens only when the cache is set almost at the same time from both servers,
            //   which should be fine and acceptable.
            return System.Diagnostics.Stopwatch.GetTimestamp();
        }

        public AggregateCache(ICache cache, string aggregateKey)
        {
            _cache = cache;
            _aggregateKey = string.Format("{0}:{1}", KeyHeader, aggregateKey);
        }

        public object Get(CacheKey cacheKey)
        {
            TransformCacheKey(cacheKey);
            try
            {
                var item = _cache.TryGet<CompositeCacheItem>(cacheKey);
                if (item == null) return null; // not in local cache

                var token = _cache.Get<long>(_aggregateKey);
                if (item.Token != token)
                {
                    // aggregateKey has been changed
                    _cache.Remove(cacheKey);
                    return null;
                }

                return item.Value;
            }
            finally
            {
                RecoverCacheKey(cacheKey);
            }
        }

        public void Set(CacheItemConfig cacheItemConfig, object value)
        {
            TransformCacheKey(cacheItemConfig.CacheKey);

            try
            {
                var token = _cache.Get<long>(_aggregateKey);
                if (token == default(long))
                {
                    token = CreateNewToken();
                    _cache.Set(_aggregateKey, token);
                }

                var item = new CompositeCacheItem { Token = token, Value = value };
                _cache.Set(cacheItemConfig, item);
            }
            finally
            {
                RecoverCacheKey(cacheItemConfig.CacheKey);
            }
        }

        public bool Remove(CacheKey cacheKey)
        {
            TransformCacheKey(cacheKey);

            try
            {
                return _cache.Remove(cacheKey);
            }
            finally
            {
                RecoverCacheKey(cacheKey);
            }
        }

        /// <summary>
        /// just remove _aggregateKey, all other keys in aggregate would dependent on _aggregateKey,
        /// and will be removed when Getting.
        /// </summary>
        public void Clear()
        {
            _cache.Remove(_aggregateKey);
        }

        #region Helpers

        private void TransformCacheKey(CacheKey cacheKey)
        {
            if (!cacheKey.CacheParameters.ContainsKey(KeyHeader))
                cacheKey.CacheParameters.Add(KeyHeader, _aggregateKey);
        }

        private static void RecoverCacheKey(CacheKey cacheKey)
        {
            if (cacheKey.CacheParameters.ContainsKey(KeyHeader))
                cacheKey.CacheParameters.Remove(KeyHeader);
        }

        #endregion

    }
}